Skip to content

refactor(build-tooling): idiomatic Groovy cleanup of the J2G output - #16661

Open
borinquenkid wants to merge 22 commits into
10.0.xfrom
groovyism/build-tooling
Open

borinquenkid wants to merge 22 commits into
10.0.xfrom
groovyism/build-tooling

Conversation

@borinquenkid

@borinquenkid borinquenkid commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Stacks on the build-tooling conversion of #16576 (build-logic/docs-core and grails-gradle/common+model). #16576 is closed and comes back as a rebuilt pull request, with a commit that only moves the files and a second that converts them. Until then the base is 10.0.x, and the diff includes that conversion.

Behaviour-preserving idiom pass over that conversion's output (14 of the 35 files in scope; the rest were already idiomatic or are interfaces/DTOs left untouched).

Applied

  • Property syntax for unambiguous getters (Class.getName() → .name, URL/File/URLConnection/Throwable/Resource accessors, Class.isArray()/.getComponentType() → .array/.componentType)
  • Map-read subscript (map.get(k) → map[k])
  • .equals() on Strings → ==
  • str.length() == 0 → isEmpty()
  • StringBuilder.append( → <<
  • String concatenation → GString where it lands in a String-typed slot
  • Dropped redundant casts after instanceof flow typing
  • x != null ? x.foo() : null → x?.foo()
  • for (X x : coll) → for (X x in coll)
  • Dropped an unnecessary public modifier on StringEscapeCategory's static methods

Deliberately left (documented per-site in the commit body): Environment.getName() (shadows Enum.name() differently), DefaultResourceLoader.getClassLoader() field-priority call, a closure-shared-variable cast the type checker rejects dropping, Class.newInstance() (avoids widening a public API's checked-exception surface), SpringIOUtils's in/out parameter names across ~8 overloads (real Tier-A hit, judged disproportionate as a standalone rename), ClassPathResource#nullSafeEquals's .equals() on an Object that may be an array, trivial getter/setter pairs that would add a public setter that doesn't exist today, and one Javadoc-bearing getter.

Found during verification: map[k] = v (subscript assignment) compiles to a dynamic ScriptBytecodeAdapter.invokeMethodN("putAt", ...) call under @CompileStatic here, unlike the read form which resolves statically. All ~10 sites introduced by this pass were reverted back to .put(k, v); the read-only subscript conversions were kept.

This revision also carries forward the rename-compliance fix from #16576 (DefaultCommandLine.groovy keeping semicolons temporarily) merged and resolved to its fully idiomatic end state — the semicolons and the @SuppressWarnings from that fix are removed here, completing the 3-stage conversion→cleanup split for that file.

Verification

  • build-logic: compile/test green (StringEscapeCategorySpec 2/2; one pre-existing, unrelated FetchTagsTaskSpec sandbox failure)
  • grails-gradle: compile green for common+model; codeStyle 0 CodeNarc violations; full test 456/456, 0 failures
  • javap -c -p dynamic-dispatch audit clean on every changed class after the putAt fix

🤖 Generated with Claude Code

borinquenkid and others added 7 commits October 10, 2026 12:01
…l to Groovy

Converts the remaining Java sources of build-logic/docs-core, grails-gradle/common and grails-gradle/model to Groovy.
Part of the per-project split requested by the maintainers on #16351; end state taken from the
9.0.x-java-to-groovy branch (origin/9.0.x..e55fd9e).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nd fix ClassPathResource.equals

- add specs for GrailsNameUtils, DefaultCommandLine and the file-resolving resources
  (file, jar and http), and extend the parser, loader, resource-utils and IO-utils specs
- ClassPathResource.equals read the other resource's classLoader through its getter,
  which falls back to the class's loader, while reading its own field; two resources
  created from the same class were therefore never equal. Compare fields on both sides,
  as the Java original did.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Behaviour-preserving idiom pass over the Java-to-Groovy conversion output for
build-logic/docs-core and grails-gradle/common+model (14 of the 35 files in
scope; the rest were already idiomatic or are interfaces/DTOs left untouched).

Applied:
- property syntax for getters with no isX/getX collision: ~35 sites
  (Class.getName() -> .name, URL.getProtocol()/.getFile()/.getPath() -> .protocol/.file/.path,
  File.getName()/.getParentFile()/.getAbsolutePath()/.getCanonicalPath(), URLConnection.getContentLength()/
  .getResponseCode(), Throwable.getMessage(), Class.isArray()/.getComponentType() -> .array/.componentType,
  Thread.getContextClassLoader(), Resource.getURL()/.getInputStream() -> .URL/.inputStream matching the
  existing test idiom)
- map.get(k)/map.containsKey(k) read -> map[k]: ~10 sites (CollectionUtils, Holder, CommandLineParser,
  DefaultCommandLine, GrailsResourceUtils, SpringIOUtils)
- .equals( on Strings -> ==: ~15 sites (GrailsResourceUtils protocol/path checks, CommandLineParser/
  DefaultCommandLine constant comparisons, FileSystemResource/UrlResource equals() field comparisons)
- str.length() == 0 / > 0 -> isEmpty()/!isEmpty(): ~12 sites
- StringBuilder.append( -> << in statement position: ~20 sites
- string concatenation -> GString where it lands in a String-typed slot (return type or exception
  message): FileSystemResource/UrlResource#getDescription, SpringIOUtils exception messages
- dropped unnecessary casts after instanceof flow typing: CollectionUtils#getOrCreateChildMap,
  GrailsResourceUtils#getPropertyNameConvention-equivalent branches, UrlResource#getInputStream,
  AbstractFileResolvingResource#contentLength/lastModified
- x != null ? x.foo() : null -> x?.foo(): SpringIOUtils#closeQuietly
- for (X x : coll) -> for (X x in coll): SpringIOUtils, DefaultCommandLine
- dropped the UnnecessaryPublicModifier on StringEscapeCategory's static methods

Deliberately left (Tier B exclusions / risk):
- Environment.getName() calls (CommandLineParser, DefaultCommandLine): Environment is an enum that
  defines its own getName()/setName(), shadowing the inherited Enum.name() the property syntax would
  otherwise resolve to differently; left as explicit calls.
- DefaultResourceLoader.getClassLoader() bare calls from getResource()/getResourceByPath(): the private
  classLoader field with the same name is accessible by direct-field priority within the declaring
  class, so property syntax there would bypass the getter's fallback-to-default logic. The one
  cross-class static-dispatch call sites (Class literal -> Class#getClassLoader()) were converted,
  matching the idiom the Spec already exercises (DefaultResourceLoader.classLoader / .defaultClassLoader).
- SupplierUtil#memoizedNonEmpty's instanceof-narrowed cast on `value` kept: the variable is shared across
  two closures assigned to the same `delegate` field, so Groovy's static type checker refuses to narrow
  it (javap/compile verified; dropping the cast failed compileGroovy with a "closure shared variable"
  error).
- GrailsResourceUtils#instantiateFromFlatConfig's Class.newInstance() kept: swapping in
  getDeclaredConstructor().newInstance() per the table entry would add NoSuchMethodException/
  InvocationTargetException to the call site, widening the method's declared checked-exception surface
  (a public API change), so it was left as-is.
- SpringIOUtils `in`/`out` parameter names across every copy() overload: a real "in is a keyword, rename
  it" Tier A hit, but touching all ~8 overloads' signatures for a non-functional rename was judged
  disproportionate to a focused idiom pass; left unchanged (it already compiles - `in` is only a Groovy
  keyword in for-loop/membership positions).
- ClassPathResource#nullSafeEquals' o1.equals(o2) kept: o1/o2 are declared Object and may be arrays;
  Groovy's == has its own (possibly different) array-comparison semantics, and the very next lines branch
  on primitive-array types via Arrays.equals, so swapping to == risked changing which branch is reached.
- Trivial getter/setter field pairs (ClassPathResource#getPath, FileSystemResource#getPath/getFile,
  DefaultCommandLine$SpecifiedOption#getOption/getValue) kept as explicit methods: collapsing them to bare
  Groovy properties would add a public setter that does not exist today, a public API change.
- XmlParserFeature#getFeatureName kept explicit: carries Javadoc that must stay on the method.

Found and fixed during verification: `map[k] = v` (subscript assignment) on a java.util.Map field
compiles to a dynamic `ScriptBytecodeAdapter.invokeMethodN("putAt", ...)` call under @CompileStatic in
this Groovy version, unlike the read form `map[k]` which resolves to the static
DefaultGroovyMethods.getAt - confirmed via the javap audit across every module. Reverted all ~10
map[k] = v sites introduced by this pass back to map.put(k, v); the read-only subscript conversions were
kept since they compile cleanly.

Verification (build-logic, own gradlew):
- :grails-docs-core:compileGroovy, :compileTestGroovy: BUILD SUCCESSFUL
- :grails-docs-core:test: 67 tests, 1 pre-existing failure unrelated to this change
  (FetchTagsTaskSpec, environmental: this sandbox's global git tag.gpgSign=true with no core.editor
  breaks `git tag`, nothing to do with StringEscapeCategory); StringEscapeCategorySpec itself 2/2 passed.
- no codenarc/checkstyle task exists for this project (compileGroovy only)

Verification (grails-gradle, own gradlew):
- :grails-gradle-common:compileGroovy, :grails-gradle-model:compileGroovy, both compileTestGroovy:
  BUILD SUCCESSFUL
- :grails-gradle-common:codeStyle, :grails-gradle-model:codeStyle: BUILD SUCCESSFUL, 0 CodeNarc
  violations in both main.html reports
- :grails-gradle-common:test (24/24) + :grails-gradle-model:test (432/432): 456/456, 0 failures,
  0 errors, 0 skipped (test-results XML, not exit code)
- javap -c -p dynamic-dispatch audit on every changed class: clean after the putAt fix above. The only
  remaining ScriptBytecodeAdapter/GroovyObject-property hits are inside the compiler-generated
  this$dist$invoke$1/this$dist$set$1/this$dist$get$1 trampolines that GrailsResourceUtils, SupplierUtil,
  DefaultResourceLoader and DefaultCommandLine already carried before this change (triggered by their
  pre-existing anonymous/nested inner classes), confirmed by diffing against untouched classes with no
  such nesting (Option, FileSystemResource, ...) which show none.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…erve rename history

git's default 50% rename-similarity threshold is what GitHub's PR view and
'git log --follow'/'blame' use to link a converted file back to its original
.java predecessor. DefaultCommandLine's full idiomatic conversion landed at
41% similarity (confirmed: GitHub shows delete+add, not a rename, losing
file history per matrei's review comment on #16576).

This keeps the method-body semicolons (76% similarity, confirmed rename-
detected, confirmed git log --follow/blame trace through to the real
pre-conversion commits and author) while still applying every other idiom
fix: dropped the public modifier, the 7 unnecessary java.util.* imports,
and the 4 non-interpolated double-quoted string literals CodeNarc also
flags. @SuppressWarnings('UnnecessarySemicolon') marks this as a known,
temporary, scoped exception pending the stacked groovyism-cleanup PR, which
removes both the semicolons and this suppression.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…uild-tooling

# Conflicts:
#	grails-gradle/model/src/main/groovy/org/grails/build/parsing/DefaultCommandLine.groovy
Copilot AI balanced review requested due to automatic review settings October 17, 2026 01:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

borinquenkid and others added 6 commits October 17, 2026 22:53
…by the rename-fix

580bd5e rebuilt DefaultCommandLine from the .java original, which carries
no annotations, and so lost the @CompileStatic stage 1 had put on the class
and the @PackageScope on systemProperties, undeclaredOptions, declaredOptions
and remainingArgs. Without them the four fields became public Groovy
properties with generated accessors and the class fell back to dynamic
dispatch. Both are restored; similarity stays at R069.

grails-gradle-model: compile, codenarc and 432 tests pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…uild-tooling

# Conflicts:
#	grails-gradle/model/src/main/groovy/org/grails/build/parsing/DefaultCommandLine.groovy
Conflicts: GrailsResourceUtils.groovy takes upstream's getPathFromRoot change
(18d8daf); the four grails-gradle/model specs upstream added in 186b59c
with the same names as this branch's are kept verbatim, with the nine feature
methods only this branch's versions asserted appended.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…stem interaction specs

Backport of #16664 (ff463cc on 7.2.x, merged up to 9.0.x) to the three
10.0.x copies of the spec, so the Windows shards pass until the 9.0.x merge-up
reaches 10.0.x; the lines are identical, so that merge stays clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…10.0.x

10.0.x was branched from 9.1.x without the version bump, so gradle.properties
still said projectVersion=9.1.0-SNAPSHOT and githubBranch=9.1.x. The
'Validate githubBranch' job therefore fails on every pull request whose base
is 10.0.x. Same four files as the 9.1.x bump in 829de60.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit fb8ecf0)
@codecov

codecov Bot commented Oct 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.02326% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.1063%. Comparing base (0725501) to head (fc8c30d).

Files with missing lines Patch % Lines
...y/org/grails/io/support/GrailsResourceUtils.groovy 89.7959% 2 Missing and 3 partials ⚠️
.../org/grails/build/parsing/CommandLineParser.groovy 90.0000% 0 Missing and 2 partials ⚠️
...org/grails/build/parsing/DefaultCommandLine.groovy 97.0149% 0 Missing and 2 partials ⚠️
.../groovy/org/grails/io/support/SpringIOUtils.groovy 87.5000% 1 Missing and 1 partial ⚠️
...src/main/groovy/grails/util/GrailsNameUtils.groovy 96.1538% 0 Missing and 1 partial ⚠️
...le/model/src/main/groovy/grails/util/Holder.groovy 66.6667% 0 Missing and 1 partial ⚠️
...ovy/org/grails/io/support/ClassPathResource.groovy 85.7143% 0 Missing and 1 partial ⚠️
...in/groovy/org/grails/io/support/UrlResource.groovy 83.3333% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@                               Coverage Diff                               @@
##             refactor/java-to-groovy-build-tooling     #16661        +/-   ##
===============================================================================
+ Coverage                                  64.0971%   64.1063%   +0.0092%     
+ Complexity                                   28681      28667        -14     
===============================================================================
  Files                                         2260       2260                
  Lines                                       112147     112145         -2     
  Branches                                     20366      20366                
===============================================================================
+ Hits                                         71883      71892         +9     
+ Misses                                       31140      31124        -16     
- Partials                                      9124       9129         +5     
Files with missing lines Coverage Δ
...src/main/groovy/grails/util/CollectionUtils.groovy 100.0000% <100.0000%> (ø)
...main/groovy/org/grails/build/parsing/Option.groovy 100.0000% <100.0000%> (ø)
...ls/io/support/AbstractFileResolvingResource.groovy 90.7407% <100.0000%> (ø)
...org/grails/io/support/DefaultResourceLoader.groovy 100.0000% <100.0000%> (ø)
...vy/org/grails/io/support/FileSystemResource.groovy 96.8750% <100.0000%> (ø)
...src/main/groovy/grails/util/GrailsNameUtils.groovy 95.1220% <96.1538%> (-0.0237%) ⬇️
...le/model/src/main/groovy/grails/util/Holder.groovy 92.0000% <66.6667%> (ø)
...ovy/org/grails/io/support/ClassPathResource.groovy 71.2644% <85.7143%> (ø)
...in/groovy/org/grails/io/support/UrlResource.groovy 90.0000% <83.3333%> (ø)
.../org/grails/build/parsing/CommandLineParser.groovy 95.2756% <90.0000%> (ø)
... and 3 more

... and 7 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

borinquenkid and others added 3 commits October 19, 2026 19:42
…(revert before merge)

Pushes, rebases and force-pushes to this stacked PR start no jobs; the PR closest to the release branch keeps CI.
Merging with this commit is a blocker.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@borinquenkid

Copy link
Copy Markdown
Member Author

BLOCKER before merge: this branch carries a temporary CI commit.

This PR is stacked, and to avoid starting a full CI matrix on every push or rebase of the stack it carries one temporary commit, CHANGEME FOR ACCEPTANCE: disable PR workflow triggers. It changes the pull_request: trigger of 13 workflows to branches: ['CHANGEME-FOR-ACCEPTANCE'], a branch that does not exist, so pushing, rebasing or force-pushing this branch starts no jobs.

Condition for merging this PR: that commit must be reverted on this branch first, and CI must then run and be green. The absence of CI checks on this PR is deliberate and does not mean the code was tested. The full build runs on #16576 and #16572, the PRs that target 10.0.x.

…uild-tooling

# Conflicts:
#	.github/workflows/benchmark.yml
#	.github/workflows/build-health.yml
#	.github/workflows/codeanalysis.yml
#	.github/workflows/codeql.yml
#	.github/workflows/codestyle.yml
#	.github/workflows/coverage.yml
#	.github/workflows/end-to-end.yml
#	.github/workflows/gradle.yml
#	.github/workflows/groovy-snapshot-canary.yml
#	.github/workflows/rat.yml
#	.github/workflows/sitemesh2.yml
#	.github/workflows/validate-actions.yml
#	.github/workflows/vulnerability-scan.yml
@testlens-app

testlens-app Bot commented Oct 22, 2026

Copy link
Copy Markdown

🔎 No tests executed 🔎

🏷️ Commit: 2879358
▶️ Tests: 0 executed
⚪️ Checks: 42/42 completed


Learn more about TestLens at testlens.app/docs.

@borinquenkid
borinquenkid changed the base branch from refactor/java-to-groovy-build-tooling to 10.0.x October 23, 2026 20:53

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants